import {
  Inject,
  Input,
  Output,
  ViewChild,
  ElementRef,
  Component,
  OnInit,
  OnDestroy,
  ViewEncapsulation,
  ChangeDetectorRef,
  ChangeDetectionStrategy,
  forwardRef,
  EventEmitter
} from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { fromEvent, merge, Observable, Subscription } from 'rxjs';
import { filter, tap, pluck, map, distinctUntilChanged, takeUntil } from 'rxjs/internal/operators';
import { inArray } from '@utils/array';
import { limitNumberInRange, getPercent } from '@utils/number';
import { sliderEvent, getElementOffset } from './wyy-slider-helper';
import { SliderEventObserverConfig, SliderValue } from './wyy-sider-types';

@Component({
  selector: 'app-wyy-slider',
  templateUrl: './wyy-slider.component.html',
  styleUrls: ['./wyy-slider.component.less'],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => WyySliderComponent),
      multi: true
    }
  ]
})
export class WyySliderComponent implements OnInit, OnDestroy, ControlValueAccessor {
  value: SliderValue = null;
  offset: SliderValue = null;
  private sliderDom: HTMLDivElement;
  private dragStart$: Observable<number>;
  private dragMove$: Observable<number>;
  private dragEnd$: Observable<Event>;
  private unDragStart$: Subscription | null;
  private unDragMove$: Subscription | null;
  private unDragEnd$: Subscription | null;
  private drag = false;
  @Input() wyyVertical = false;
  @Input() wyyMin = 0;
  @Input() wyyMax = 100;
  @Input() bufferOffset: SliderValue = 0;
  @Output() wyySliderChange = new EventEmitter<SliderValue>();
  @ViewChild('wyySlider', { static: true }) private wyySlider: ElementRef;
  constructor(@Inject(DOCUMENT) private doc: Document, private cdr: ChangeDetectorRef) {}

  ngOnInit() {
    this.sliderDom = this.wyySlider.nativeElement;
    this.crateDragObservable();
    this.subscribeDrag(['start']);
  }
  private crateDragObservable() {
    const orient = this.wyyVertical ? 'pageY' : 'pageX';
    const mouse: SliderEventObserverConfig = {
      start: 'mousedown',
      move: 'mousemove',
      end: 'mouseup',
      filter: (e: MouseEvent) => e instanceof MouseEvent,
      pluckKey: [orient]
    };
    const touch: SliderEventObserverConfig = {
      start: 'touchstart',
      move: 'touchmove',
      end: 'touchend',
      filter: (e: TouchEvent) => e instanceof TouchEvent,
      pluckKey: ['touches', '0', orient]
    };
    [mouse, touch].forEach(source => {
      const { start, move, end, filter: filterFunc, pluckKey } = source;
      source.startPlucked$ = fromEvent(this.sliderDom, start)
        .pipe(
          filter(filterFunc),
          tap(sliderEvent),
          pluck(...pluckKey),
          map((position: number) => this.findDoneValue(position))
        );
      source.endPlucked$ = fromEvent(this.doc, end);
      source.movePlucked$ = fromEvent(this.doc, move)
        .pipe(
          filter(filterFunc),
          tap(sliderEvent),
          pluck(...pluckKey),
          distinctUntilChanged(),
          map((position: number) => this.findDoneValue(position)),
          takeUntil(source.endPlucked$)
        );
    });
    this.dragStart$ = merge(mouse.startPlucked$, touch.startPlucked$);
    this.dragMove$ = merge(mouse.movePlucked$, touch.movePlucked$);
    this.dragEnd$ = merge(mouse.endPlucked$, touch.endPlucked$);
  }
  private subscribeDrag(events: string[] = ['start', 'move', 'end']) {
    if (inArray(events, 'start') && this.dragStart$ && !this.unDragStart$) {
      this.unDragStart$ = this.dragStart$.subscribe(this.onDragStart.bind(this));
    }
    if (inArray(events, 'move') && this.dragMove$ && !this.unDragMove$) {
      this.unDragMove$ = this.dragMove$.subscribe(this.onDragMove.bind(this));
    }
    if (inArray(events, 'end') && this.dragEnd$ && !this.unDragEnd$) {
      this.unDragEnd$ = this.dragEnd$.subscribe(this.onDragEnd.bind(this));
    }
  }
  private unSubscribeDrag(events: string[] = ['start', 'move', 'end']) {
    if (inArray(events, 'start') && this.unDragStart$) {
      this.unDragStart$.unsubscribe();
      this.unDragStart$ = null;
    }
    if (inArray(events, 'move') && this.unDragMove$) {
      this.unDragMove$.unsubscribe();
      this.unDragMove$ = null;
    }
    if (inArray(events, 'end') && this.unDragEnd$) {
      this.unDragEnd$.unsubscribe();
      this.unDragEnd$ = null;
    }
  }
  private valueEqual(oldValue: SliderValue, newValue: SliderValue): boolean {
    if (typeof newValue !== typeof oldValue) {
      return false;
    }
    return oldValue === newValue;
  }
  private setValue(value: SliderValue, needCheck = false) {
    if (needCheck) {
      if (this.drag) {
        return;
      }
      this.value = this.formatValue(value);
    } else if (!this.valueEqual(this.value, value)) {
      this.value = value;
      this.onValueChange(value);
    }
    this.upDateTracksAndHandles();
  }
  private formatValue(value: SliderValue) {
    let res = value;
    if (this.assertValueValid(value)) {
      res = this.wyyMin;
    } else {
      res = limitNumberInRange(value, this.wyyMin, this.wyyMax);
    }
    return res;
  }
  private assertValueValid(value: SliderValue) {
    return isNaN(typeof value !== 'number' ? parseFloat(value) : value);
  }
  private upDateTracksAndHandles() {
    this.offset = this.getValueToOffset(this.value);
    // 手动触发变更检测
    this.cdr.markForCheck();
  }
  private getValueToOffset(value: SliderValue): SliderValue {
    return getPercent(this.wyyMin, this.wyyMax, value);
  }
  private onDragStart(value: number) {
    this.toggleDragMoving(true);
    this.setValue(value);
  }
  private onDragMove(value: SliderValue) {
    if (this.drag) {
      this.setValue(value);
      // 手动触发变更检测
      this.cdr.markForCheck();
    }
  }
  private onDragEnd() {
    this.wyySliderChange.emit(this.value);
    this.toggleDragMoving(false);
    // 手动触发变更检测
    this.cdr.markForCheck();
  }
  private toggleDragMoving(moving: boolean) {
    this.drag = moving;
    if (moving) {
      this.subscribeDrag(['move', 'end']);
    } else {
      this.unSubscribeDrag(['move', 'end']);
    }
  }
  private findDoneValue(position: number): number {
    // 获取播放器滑块的实际长度
    const sliderLen = this.getSliderLength();
    // 获取播放器滑块左/上端点的位置
    const sliderStart = this.getSliderStartPosition();
    const ration = limitNumberInRange((position - sliderStart) / sliderLen, 0, 1);
    const rationTrue = this.wyyVertical ? 1 - ration : ration;
    return rationTrue * (this.wyyMax - this.wyyMin) + this.wyyMin;
  }
  private getSliderLength() {
    return this.wyyVertical ? this.sliderDom.clientHeight : this.sliderDom.clientWidth;
  }
  private getSliderStartPosition() {
    const offset = getElementOffset(this.sliderDom);
    return this.wyyVertical ? offset.top : offset.left;
  }
  private onValueChange(value: SliderValue): void {}
  private onValueTouched(): void {}
  writeValue(value: SliderValue): void {
    this.setValue(value);
  }
  registerOnChange(fn: (value: SliderValue) => void): void {
    this.onValueChange = fn;
  }
  registerOnTouched(): void {
    this.onValueTouched();
  }
  ngOnDestroy(): void {
    this.unSubscribeDrag();
  }
}
